/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2010 huliqing, huliqing.cn@gmail.com
 *
 * This file is part of QFaces.
 * QFaces is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * QFaces is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with QFaces.  If not, see <http://www.gnu.org/licenses/>.
 *
 * 这个文件是QFaces的一部分。
 * 您可以单独使用或分发这个文件，但请不要移除这个头部声明信息.
 * QFaces是一个自由软件，您可以自由分发、修改其中的源代码或者重新发布它，
 * 新的任何修改后的重新发布版必须同样在遵守LGPL3或更后续的版本协议下发布.
 * 关于LGPL协议的细则请参考COPYING、COPYING.LESSER文件，
 * 您可以在QFaces的相关目录中获得LGPL协议的副本，
 * 如果没有找到，请连接到 http://www.gnu.org/licenses/ 查看并获取副本。
 *
 * - Author: Huliqing
 * - Contact: huliqing.cn@gmail.com
 * - License: GNU Lesser General Public License (LGPL)
 * - Blog and source code availability: http://www.huliqing.name/
 */


package name.huliqing.qfaces.component;
 
import java.io.IOException;
import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.el.MethodExpression;
import javax.faces.application.FacesMessage;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.component.UIComponentBase;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.Converter;
import name.huliqing.qfaces.QFaces;

/**
 * 
 * @author huliqing
 */
public class Validator extends UIComponentBase implements Ajax{
	private final static Logger logger = Logger.getLogger(Validator.class.getName());
	
	// ---------------------------------------- 外部参数
	
	// 指定到当前组件所服务的目标组件，一般应该指定到inputText,inputHidden,
	// inputTextarea,selectMany...,selectOne...等能够保存值的组件
	// 也就是说，for的值应该为某个InputText...组件的id
	private String _for;
	
	// 指定到另一个目标组件ID，如果指定了equalId,那么这个组件的值必须
	// 与equalId所指定的目标组件的值相等，这样才能通过验证。(equal是相等的意思，
	// 不单指数字相等，字符也要完全相等)
	private String equalId;
	
	// 对于目标组件输入值的提示信息，如在用户注册帐号时，我们经常会这样提示用户：
	// "请输入你的用户名，长度不能小于6个字符，并且不能大于30个字符",以帮助用户
	// 输入正确的值。
	private String message;

	// 当验证出错时要显示的信息,如果不设置，则默认使用message
	private String messageError;
	
	// 当验证正确时要显示的信息,如果不设置，则默认使用message
	private String messageCorrect;
	
	// 定义允许填写的字符串最小长度
	private Integer minLength;
	
	// 定义允许填写的字符串最大长度
	private Integer maxLength;
	
	// 定义目标允许填的最小值,也就是目标值不能小于这个值
	// 这个限制只有目标为数值类型，并且有填值时才有效.
	// 你可以通过组件的required,type属性来配合这个属性,起到验证数字的作用
	private Long min;
	
	// 定义目标允许填的最大值，也就是目标值不能大于这个值
	// 这个限制只有目标为数值类型，并且有填值时才有
	// 请参考属性“min"
	private Long max;
	
	// 定义目标是否是必填值的
	private Boolean required;
	
	// 定义目标的类型，该参数的取值必须为以下数据
	// "String"		-> 任何字符
	// "Number" 	-> 数字类型,不分正负，整数，小数
	// "Integer"	-> 表示整数，不分正负，整数
	private String type;
	
	
	// 正则表达式,该表达式用于验证目标组件值,通过正则表达式可以用
	// 相对简单的表达式来达到其它简单属性所无法达到的验则规则,几个参考示例：<BR />
	// ^[a-zA-Z]+$			-> 匹配所有英文字母
	// ^[a-z]+$				-> 匹配所有小写英文字母
	// ^[A-Z]+$				-> 匹配所有大写英文字母
	// ^\d+$ 或 ^[0-9]+$		-> 匹配所有无符号的正整数。（阿拉伯数字）
	// ^\+?\d+$				-> 匹配所有正整，（可带+号）
	// ^\-\d+$				-> 匹配所有负整数（带-号）
	// ^[0-9]*\.?[0-9]*$	-> 匹配所有无符号的浮点数
	// ^\+?[0-9]*\.?[0-9]*$	-> 匹配所有浮点数（正数）
	// ^\-[0-9]*\.?[0-9]*$	-> 匹配所有浮点数（负数）
	private String regex;
	
	// 绑定到后台某个方法，方法签名应该像这样：public Boolean listener(String arg)
	// 这个方法绑定主要是让用户能够以更高级的方式验证目标组件的值。该方法会以Ajax的方法将前台页
	// 面目标组件的值通过Ajax方式传递到后台作为这个方法的参数(arg)，通过这个方法对
	// 组件值进行验证，并返回true/false来确认目标组件的值是否允许,这属于前端验证
	// 在生命周期的验证阶段，这个方法会再次调用（服务端验证），来保证目标组件的值是允许的
	private MethodExpression listener;
	
	// 触发类型，该属性指定前端页面如何触发组件的验证。可指定的属性值如下：
	// onblur -> 鼠标离开目标组件焦点时.
	// interval -> 每间隔一个时间段验证一次 .
	private String event;

	// 当event属性指定为"interval"时，这个属性才有作用
	// 这个属性用于指定interval验证方式时的时间间隔.这个属性不会导致无休止的验证,
	// 每次要触发验证时都应该先检查当前目标组件值是否与最后一次验证时的值是否一致。
	// 如果一致则不再验证，否则会造成 listener属性不停调用Ajax(如果设置了listener属性)
	private Integer interval;
	
	// 绑定到JavaScript Function属性，当目标组件值被验证不通过时执行的javascript函数
	private String onError;
	
	// 绑定到JavaScript Function属性，当目标组件值被验证通过时执行的javascript函数
	private String onCorrect;
	
	// 是否显示图标,打开这个属性，可以让组件在验证正确或错误时显示一个不同的图标
	// 通过imgSrcCorrect,imgSrcError属性可以指定这两个不同的图标
	private Boolean displayFlag;
	
	// 当发生验证错误时，显示的图标的URL
	private String imgSrcError;
	
	// 当发生验证正确时，显示的图标的URL
	private String imgSrcCorrect;
	
	// Message的默认样式
	private String styleClass;
	
	// 当发生验证错误时，message的样式
	private String styleClassError;
	
	// 当发生验证正确时，message的样式
	private String styleClassCorrect;
	
	// 当发生验证错误时，目标组件的样式
	private String styleClassTargetError;
	
	// 当发生验证正确时， 目标组件的样式
	private String styleClassTargetCorrect;
	
	// ---------------------------------------- 内部参数
	
	// 整个组件的ID
	private String ID_CLIENT;
	
	// 组件Flag图标区域的ID
	private String ID_FLAG;
	
	// 组件message的ID
	private String ID_MESSAGE;
	
	// 该组件的当前状态,定义如下:
	// -1	-> 发生验证错误的
	// 0	-> 状态未知的，也就是未经过验证的
	// 1	-> 经过验证为正确的
	private Integer state;
	
	public Validator() {
		super();
		setRendererType(null);
	}

    @Override
    public Object saveState(FacesContext fc) {
    	Object[] obj = new Object[10];
    	obj[0] = super.saveState(fc);
    	obj[1] = listener;
    	obj[2] = message;
    	obj[3] = minLength;
    	obj[4] = maxLength;
    	obj[5] = min;
    	obj[6] = max;
    	obj[7] = required;
    	obj[8] = type;
    	obj[9] = regex;
    	return obj;
    	// 这两个ID参数不应该保存状态，因为在渲染过程中，ID可能是动态变更的
    	// 如在DataTable中
//    	this._for
//    	this.equalId
    }
    
    @Override
    public void restoreState(FacesContext fc, Object o) {
    	Object[] obj = (Object[]) o;
    	super.restoreState(fc, obj[0]);
    	listener = (MethodExpression) obj[1];
    	message = (String) obj[2];
    	minLength = (Integer) obj[3];
    	maxLength = (Integer) obj[4];
    	min = (Long) obj[5];
    	max = (Long) obj[6];
    	required = (Boolean) obj[7];
    	type = (String) obj[8];
    	regex = (String) obj[9];
    }
    
    public MethodExpression getListener() {
		return listener;
	}

	public void setListener(MethodExpression listener) {
		this.listener = listener;
	}

	@Override
    public void encodeBegin(FacesContext fc) throws IOException {
		// 载入资源
		QFaces.getResLoader2().loadJavaScript(this, "Validator.js");

		// ---- 设定ID
		
    	this.ID_CLIENT = this.getClientId(fc);
    	this.ID_FLAG = this.ID_CLIENT + ":flag";
    	this.ID_MESSAGE = this.ID_CLIENT + ":message";
    	
    	// ---- 载入参数
    	
    	this.message = (String) QFaces.getAttribute(this, "message", "");
    	this.messageError = (String) QFaces.getAttribute(this, "messageError", this.message);
    	this.messageCorrect = (String) QFaces.getAttribute(this, "messageCorrect", this.message);
    	this.minLength = QFaces.convertToInteger(QFaces.getAttribute(this, "minLength", null));
    	this.maxLength = QFaces.convertToInteger(QFaces.getAttribute(this, "maxLength", null));
    	this.min = QFaces.convertToLong(QFaces.getAttribute(this, "min", null));
    	this.max = QFaces.convertToLong(QFaces.getAttribute(this, "max", null));
    	this.required = QFaces.convertToBoolean(QFaces.getAttribute(this, "required", Boolean.FALSE));
    	this.type = (String) QFaces.getAttribute(this, "type", "String");
    	this.regex = (String) QFaces.getAttribute(this, "regex", "");
    	this.event = (String) QFaces.getAttribute(this, "event", "onblur");
    	this.interval = QFaces.convertToInteger(QFaces.getAttribute(this, "interval", 100)); // 默认100毫秒检测一次
    	this.onError = (String) QFaces.getAttribute(this, "onError", "");
    	this.onCorrect = (String) QFaces.getAttribute(this, "onCorrect", "");
    	this.displayFlag = QFaces.convertToBoolean(QFaces.getAttribute(this, "displayFlag", Boolean.TRUE));
    	this.imgSrcError = (String) QFaces.getAttribute(this, "imgSrcError", 
    			QFaces.getResLoader2().makeImageSrc("05-error.gif"));
    	this.imgSrcCorrect = (String) QFaces.getAttribute(this, "imgSrcCorrect", 
    			QFaces.getResLoader2().makeImageSrc("05-correct.gif"));
    	this.styleClass = (String) QFaces.getAttribute(this, "styleClass", "_qf_05_styleClass");
    	this.styleClassError = (String) QFaces.getAttribute(this, "styleClassError", "_qf_05_styleClassError");
    	this.styleClassCorrect = (String) QFaces.getAttribute(this, "styleClassCorrect", "_qf_05_styleClass"); // 与默认同一个样式
    	this.styleClassTargetError = (String) QFaces.getAttribute(this, "styleClassTargetError", "");
    	this.styleClassTargetCorrect = (String) QFaces.getAttribute(this, "styleClassTargetCorrect", "");
    	
    	// 载入for,equal参数，必须的.
    	// for是必须要的属性，当不存在时，应该给予提示.
    	UIComponent target = this.findTargetComponent(fc);
    	findEqualComponent(fc);
    	if (target == null) 
    		logger.warning("Target Component not found, check attribute \"for\" please.");
    	
    	// "for" "equalId" 所指定的Component, 请参考   findTargetComponent and findEqualComponent
    	// 以下代码移除
    	// -------------------------------------------------
//    	this._for = (String) this.getAttributes().get("for");
//    	this.equalId = (String) this.getAttributes().get("equalId");
//    	this._for = (String) this.getAttributes().get("for");
//    	this.equalId = (String) QFaces.getAttribute(this, "equalId", "");
  
//    	// ---- 处理参数，确保参数正确
//    	if (this._for == null)
//    		throw new NullPointerException("The Attribute \"for\" required!");
//    	
//    	// 将for属性所指定的ID转化为长的客户端ID,找不到则不管，因为有可能为一般的HTML组件的ID
//    	UIComponent target = findComponent(this._for);
//    	if (target != null) 
//    		this._for = target.getClientId(fc);
//    	
//    	// 作用同上
//    	if (!this.equalId.equals("")) {
//    		UIComponent eqTarget = findComponent(this.equalId);
//    		if (eqTarget != null)
//    			this.equalId = eqTarget.getClientId(fc);
//    	}
    	// -------------------------------------------------
    	
    	// 首次渲染时一定处于未验证状态，在回传时可能就不一定
    	if (this.state == null)
    		this.state = 0;
    	
    	// 确保默认响应类型
    	if (!"onblur".equalsIgnoreCase(this.event) && !"interval".equalsIgnoreCase(this.event)) 
    		this.event = "onblur";
    	
    	// ---- 渲染
    	
    	ResponseWriter rw = fc.getResponseWriter();
    	rw.startElement("span", this);
    	rw.writeAttribute("id", this.ID_CLIENT, "id");
    	rw.writeAttribute("style", "border:0px solid red;", null);

    		// render flag
    		rw.startElement("span", this);
    		rw.writeAttribute("id", this.ID_FLAG, null);
    		rw.writeAttribute("style", "vertical-align:middle;border:0px solid green;", null);
    		rw.endElement("span");
    		
    		// render message
    		rw.startElement("span", this);
    		rw.writeAttribute("id", this.ID_MESSAGE, null);
    		rw.writeAttribute("style", "border:0px solid blue;", null);
    		rw.endElement("span");
    	rw.endElement("span");
    }
	
	@Override
    public void encodeEnd(FacesContext fc) throws IOException {		
		// Java与JavaScript的正则在“\”的转义上存在一些差异
		String regexForJS = "";
		if (regex.indexOf("\\") != -1) {
			regexForJS = regex.replaceAll("\\\\", "\\\\\\\\");
		} else {
			regexForJS = regex;
		}
		
        StringBuilder jsInit = new StringBuilder("Q.F.UIValidator.init(")
        		.append(" \"" + this.getClass().getName() + "\"")
                .append(",\"" + this.ID_CLIENT + "\"")
                .append(",\"" + this.ID_FLAG + "\"")
                .append(",\"" + this.ID_MESSAGE + "\"")
                .append(",\"" + this.state + "\"")
                .append(",\"" + this._for + "\"")
                .append(",\"" + this.message + "\"")
                .append(",\"" + this.messageError + "\"")
                .append(",\"" + this.messageCorrect + "\"")
                .append(",\"" + (this.minLength != null ? this.minLength : "") + "\"")
                .append(",\"" + (this.maxLength != null ? this.maxLength : "") + "\"")
                .append(",\"" + (this.min != null ? this.min : "") + "\"")
                .append(",\"" + (this.max != null ? this.max : "") + "\"")
                .append(",\"" + this.required.toString() + "\"")
                .append(",\"" + this.type + "\"")
                .append(",\"" + this.equalId + "\"")
                .append(",\"" + regexForJS + "\"")
                .append(",\"" + (this.listener != null ? this.listener.getExpressionString() : "") + "\"")
                .append(",\"" + this.event + "\"")
                .append(",\"" + this.interval + "\"")
                .append(",\"" + this.onError + "\"")
                .append(",\"" + this.onCorrect + "\"")
                .append(",\"" + this.displayFlag.toString() + "\"")
                .append(",\"" + this.imgSrcError + "\"")
                .append(",\"" + this.imgSrcCorrect + "\"")
                .append(",\"" + this.styleClass + "\"")
                .append(",\"" + this.styleClassError + "\"")
                .append(",\"" + this.styleClassCorrect + "\"")
                .append(",\"" + this.styleClassTargetError + "\"")
                .append(",\"" + this.styleClassTargetCorrect + "\"")
                .append(")");
        QFaces.getResLoader2().loadJavaScriptInit(this, jsInit.toString());
	}
	
	public void ajax(Map<String, String> params, FacesContext fc) {
        String listenerExp = params.get("listenerExp");
        String targetValue = params.get("targetValue");
        Boolean result = this.validateListener(fc, targetValue, listenerExp);
        QFaces.getResponse().responseHTML(fc, result.toString());
	}
	
	@Override
	public void decode(FacesContext fc) {}
	
	// 获取某个目标组件的表单提交值
	private String getSubmitValue(FacesContext fc, UIComponent ui) {
		if (ui == null)
			return null;

		if (!(ui instanceof EditableValueHolder)) {
			throw new RuntimeException("Not a EditableValueHolder component!" +
					"目标组件必须是能够保存值的,如那些继承自" +
					"EditableValueHolder的组件，InputText,SelectOne...等");
		}
		String resultValue = null;
		EditableValueHolder holder = (EditableValueHolder) ui;
		// 先获取value(可能是已经通过验证的了),如果不存在value,则获取submitValue
		Object value = holder.getValue();
		Object submitValue = holder.getSubmittedValue();
		if (value != null) {
			// 如果目标组件存在转换器，则需要将目标值转换回去(String)
			// 这样才能与页面js的验证方式同步
			Converter con = holder.getConverter();
			if (con != null)
				resultValue = con.getAsString(fc, ui, value);
			else
				resultValue = value.toString();
		} else if(submitValue != null) {
			resultValue = submitValue.toString();
		}
		return resultValue;
	}
	
	// 验证两个值是否一致,只比较字符是否一样。
	// 如果equalId为null或空，则不验证（返回true)
	private boolean validateEqual(FacesContext fc, String targetValue) {
		UIComponent equal = findEqualComponent(fc);
		if (equal == null) {
			return true; // 这表示没有添加equalId属性。
		}
		String equalValue = this.getSubmitValue(fc, equal);
		
		String hasValue = targetValue != null ? targetValue : equalValue;
		// 同为null
		if (hasValue == null)
			return true;
		return (hasValue.equals(targetValue) && hasValue.equals(equalValue));
	}
	
	// 根据组件绑定的length属性来验证目标组件的值是否在长度限制之内
	// 如果targetValue为null,则返回true
	private boolean validateLength(String targetValue) {
		if (isNoValue(targetValue))
			return true;
		if (minLength != null && targetValue.length() < minLength)
			return false;
		if (maxLength != null && targetValue.length() > maxLength)
			return false;
		return true;
	}
	
	// 根据组件绑定的Listener来验证值是否正确
	private boolean validateListener(FacesContext fc, String targetValue, String listenerExp) {
		if (isNoValue(targetValue))
			return true;
		MethodExpression _listener = QFaces.createMethodExpression(listenerExp,
                Boolean.class,
                new Class[]{String.class});
		Object[] args = {targetValue};
		Boolean result = (Boolean) _listener.invoke(fc.getELContext(), args);
		// 如果绑定的listener返回null,则默认认为通过验证
		if (result == null)
			result = true;
		return result;
	}
	
	// 根据组件所设定的正则表达式来验证目标值是否匹配
	// 如果regex为null或空，则返回true
	// 如果targetValue为null或空，则返回true
	private boolean validateRegex(String targetValue) {
		logger.info("regex=" + regex);
		if (isNoValue(regex) || isNoValue(targetValue))
			return true;
		return Pattern.compile(regex).matcher(targetValue).matches();
	}
	
	// 根据组件所设定的required属性来验证目标组件的值
	// 如果required没有设置，则返回true
	private boolean validateRequired(String targetValue) {
		if (required != null && required) {
			return (!isNoValue(targetValue));			
		}
		return true;
	}
	
	// 根据组件所设定的type属性来验证目标组件的值是否符合要求
	// 如果目标值targetValue为null或空，则不验证（返回true)
	// 如果type=null或空,则不验证(返回true)
	private boolean validateType(String targetValue) {
		if (isNoValue(targetValue) || isNoValue(type))
			return true;
		if (type.equalsIgnoreCase("Integer")) {
			return isInteger(targetValue);
		} else if (type.equalsIgnoreCase("Number")) {
			return isNumber(targetValue);
		}
		// Other case 3, type = String
		return true;
	}
	
	// 根据组件所设定的min,max属性来验证目标组件的值是否符合要求
	// 该方法只判断数字类型，
	// 如果目标值不能转换为数字，则不验证（返回true）
	// 如果目标值为null或空，则不验证（返回true)
	private boolean validateVolume(String targetValue) {
		if (isNoValue(targetValue))
			return true;
		if (isInteger(targetValue)) {
			Long temp = Long.parseLong(targetValue);
			if (min != null && temp < min) 
				return false;
			if (max != null && temp > max)
				return false;
		} else if (isNumber(targetValue)) {
			Double temp = Double.parseDouble(targetValue);
			if (min != null && temp < min) 
				return false;
			if (max != null && temp > max)
				return false;
		}
		return true;
	}
	
	// 判断是否为null或者空
	private boolean isNoValue(String targetValue) {
		return (targetValue == null || "".equals(targetValue));
	}
	
	private boolean isNumber(String targetValue) {
		return Pattern.compile("^-?\\d*\\.?\\d+$").matcher(targetValue).matches();
	}
	
	private boolean isInteger(String targetValue) {
		return Pattern.compile("^-?\\d+$").matcher(targetValue).matches();
	}
	
	@Override
	public String getFamily() {
		return null;
	}
	
	
	@Override
	public void processValidators(FacesContext fc) {
		super.processValidators(fc);
		UIComponent target = findTargetComponent(fc);
		if (target == null) {
			logger.warning("Target component not found!");
			return;
		}
		// 预设state = 1,表示正确的。
		this.state = 1;
		if (!validate(fc, target, getSubmitValue(fc, target))) {
			this.state = -1;
			// Fix bug, 在不能通过验证时，必须添加一个message,否则可能造成同一
			// form下的其它组件的submitValue丢失,特别是在DataTable下的论询组件
			fc.addMessage(target.getClientId(fc), new FacesMessage("Validate Error"));
			// 转到渲染阶段 
			fc.renderResponse();
		}
	}

	private boolean validate(FacesContext fc, UIComponent target, String targetSubmitValue) {
		if (!validateRequired(targetSubmitValue)) {
			logger.info("failure -> validateRequired");
			return false;
		}
		
		if (!validateLength(targetSubmitValue)) {
			logger.info("failure -> validateLength");
			return false;
		}

		if (!validateEqual(fc, targetSubmitValue)) {
			logger.info("failure -> validateEqual");
			return false;
		}
		
		if (!validateRegex(targetSubmitValue)) {
			logger.info("failure -> validateRegex");
			return false;
		}
		
		if (!validateType(targetSubmitValue)) {
			logger.info("failure -> validateType");
			return false;
		}
		
		if (!validateVolume(targetSubmitValue)) {
			logger.info("failure -> validateVolume");
			return false;
		}
		
		if (listener != null 
				&& !validateListener(fc, targetSubmitValue, listener.getExpressionString())) {
			logger.info("failure -> validateListener");
			return false;
		}
		return true;
	}

	private UIComponent findTargetComponent(FacesContext fc) {
		this._for = (String) this.getAttributes().get("for");
		if (this._for == null)
			return null;
		// 将for属性所指定的ID转化为长的客户端ID,找不到则不管，因为有可能为一般的HTML组件的ID
    	UIComponent target = findComponent(this._for);
    	if (target != null) {
    		this._for = target.getClientId(fc);
    		return target;
    	}
    	return null;
	}
	
	private UIComponent findEqualComponent(FacesContext fc) {
		this.equalId = (String) this.getAttributes().get("equalId");
		if (this.equalId == null)
			return null;
    	UIComponent ui = findComponent(this.equalId);
    	if (ui != null) {
    		this.equalId = ui.getClientId(fc);
    		return ui;
    	}
    	return null;
	}
}






